# 架构与请求全过程

读完本章，你应能回答：用户输入之后谁在调用谁？模型做了什么？为什么最后的数字不是模型直接写出来的？

## 先理解系统的分工

可以把系统看作一个分析团队：官方界面是接待窗口，外层 Agent 是任务调度员，业务工具是专门的分析流程。工具内部，模型提出查询方案，程序检查查询结果，数学模块计算贡献，报告模块排版。各个角色之间传递的是明确的数据，而不是一段越来越长的“思考”。

当前主入口是官方服务 `http://127.0.0.1:5670`。早期独立页面对应 `commerce/main.py` 和 `commerce/static/index.html`，它用于验证业务链路；不要把它当作完整官方界面的实现。

```text
用户中文问题
    ↓
DB-GPT 官方前端 → 官方 ReAct 对话接口
    ↓ 外层模型选择工具与参数
analyze_commerce_revenue(region, as_of)
    ↓ 构造 AnalysisRequest 并进入 run_analysis
AWEL：resolve → sql_stage → verification → calculation
    ↓                         ↓              ↓
  固定口径                独立明细核对      精确贡献分解
    ↓ 整个 DAG 成功返回
save_result → evidence.json / query.sql / parameters.json / report.html
    ↓ 工具返回指标、链接、render_tool_call
官方 Agent → html_interpreter(file_path=原报告) → 展示与结束
```

图里的两次模型参与要区分：外层模型决定调用什么工具；内层 `generate_sql` 发起独立的模型请求来写 SQL。一个工具调用内部完全可以再次调用模型。内层计算不依赖外层模型“心算”。

## 框架能力和本项目实现

| 层次 | 主要职责 | 本项目使用方式 |
| --- | --- | --- |
| 官方前端与服务 | 对话、工具过程、报告展示 | 运行完整官方应用 |
| 官方 ReAct | 根据上下文选择动作，观察工具结果 | 选择专用经营分析工具，再展示报告 |
| ResourceManager 与工具机制 | 注册、描述、调用资源 | 注册 `analyze_commerce_revenue` |
| AWEL | 组织节点及依赖，执行 DAG | 串行执行四个业务节点 |
| commerce 业务层 | 口径、数据核对、计算、证据 | 本项目新增的主要代码 |
| SQLite 与模型接口 | 存储数据、生成候选 SQL | SQLite 本地数据，DeepSeek 兼容接口 |

AWEL 不是 Agent 本身。它只负责按依赖运行节点。只有 `sql_stage` 及核验失败后的修复会在内层调用模型，其余步骤是程序。给普通函数包上 `MapOperator` 不会让函数自动具有推理能力。

## 启动时发生什么

入口 `run_official.py` 先指定官方运行目录与语言，读取本地模型配置，准备持久化加密密钥和官方模板文件。随后导入官方服务，调用 `official_integration.install(server)`，最后用 `official.toml` 启动服务器。

`install` 做三件事。第一，准备模拟数据库；首次运行时，把基础数据复制到 `official-data/ecommerce.sqlite`。第二，在官方静态页面的兜底路由之前注册报告下载路由，否则报告请求可能被前端页面接走。第三，包装官方 `initialize_app`：先完成官方初始化，再注册业务工具和名为 `ecommerce` 的数据源。

这里的数据库有两个用途，不能混淆。官方的元数据库记录平台配置；`ecommerce.sqlite` 存放订单和退款。把数据源注册到平台，也不代表任意 SQL 自动有正确的业务口径。

基础数据库与官方副本目前仅在首次创建时复制。如果以后修改了基础数据，官方副本不会自动更新。这是理解“为什么命令行结果和页面结果不一致”时应首先检查的地方。

## 以一次请求逐步展开

用户提出“上个月华东地区收入下降了，分析各因素贡献”。外层 Agent 看见工具描述后，可以传入 `region=华东`、`as_of=2026-09-16`。其中 `as_of` 是分析基准日，用来确定完整自然月，并非直接作为查询开始日。

当前工具只有这两个入参。它会重新构造一条标准问题，设置 `mode=live` 后调用 `run_analysis`。因此原始自由文本并不会完整进入内部证据中的 `request.question`。如果用户在原话中增加“只看某商品”，工具内部不会因此自动增加商品筛选。扩展需求必须改契约和入参，不能只改提示词。

### 第一步 创建运行上下文

`run_analysis` 创建 `state` 字典，包括唯一 `run_id`、请求、数据库路径、来源指纹、空的 SQL 尝试列表和轨迹。这个字典会在节点间传递，每个节点补充自己的结果后返回。

```python
state = {
    "run_id": "本次唯一编号",
    "request": {"region": "华东", "as_of": "2026-09-16", "mode": "live", ...},
    "database_path": "官方电商数据库绝对路径",
    "provenance": {"database_sha256": "...", ...},
    "sql_attempts": [],
    "trace": []
}
```

这段是结构示意，省略号不是可直接执行的请求。不要把 state 理解为永久记忆；它是一次运行的上下文，最后才被写入证据文件。

### 第二步 固定范围和口径

`resolve` 调用 `periods`，得到 `previous_start=2026-07-01`、`current_start=2026-08-01`、`end=2026-09-01`。本期是八月，上期是七月。

它检查问题是否包含收入或营收、地区是否冲突、是否出现不支持的时间表达，并检查数据覆盖范围。这里是有限的规则检查，并非全面自然语言语义理解。默认基准日固定为 2026-09-16，不会自动随今天的日期变化。

### 第三步 生成候选 SQL

`sql_stage` 根据模式分支：`reference` 直接使用人工标准 SQL；`live` 调用 `generate_sql`。官方工具固定使用 live。

内层模型收到系统约束、六表 schema、业务问题和四个查询参数，返回 JSON 中的 `sql` 字符串。调用设置包含温度 0、JSON 对象格式、90 秒超时和关闭 SDK 自动重试。温度 0 不能保证每次返回完全相同 SQL；JSON 格式也不能保证 SQL 语义正确。

这些约束让问题更容易，但模型输出仍只是候选方案。它必须经过下一步才有资格参与报告。

### 第四步 独立核验

`verification` 先从原始明细算出期望结果，再执行模型 SQL，检查字段、期间渠道唯一性、数值类型和全部结果是否一致。随后检查数据库指纹是否较运行开始发生变化。

只要一次候选通过，state 才会获得供后续消费的 `rows`。如果抛出 `QueryRejected`，live 模式最多再让模型修复一次，输入包括旧 SQL 和错误说明。第二次仍失败就中止。模型网络错误、无效 JSON 并不走这个 SQL 修复分支；它们直接成为失败。

### 第五步 计算贡献

`calculation` 把核验过的六行传给 `decompose`，输出总指标、各渠道对照、四因素贡献和精确替换过程。程序此时不需要再请求模型。字段名 `cents` 表示分，展示人民币元时除以 100。

### 第六步 保存并展示

四个节点完成后，`run_analysis` 增加 report 轨迹并保存文件。注意：轨迹有五类阶段，但 AWEL 图里只有四个节点，保存报告是在 DAG 返回后执行的。

业务工具把结构化结果和 `render_tool_call` 返回外层。后者告诉官方 Agent 用 `html_interpreter` 的文件模式读原报告。这样页面上的表格和金额来自确定性文件，不必让模型再写一遍 HTML。外层的文字总结仍由模型生成，依然需要与报告核对。

## 文件阅读地图

| 文件 | 先看哪个入口 | 带着什么问题读 |
| --- | --- | --- |
| `run_official.py` | 主程序 | 官方应用如何启动 |
| `commerce/official_integration.py` | install、analyze_commerce_revenue | 工具如何接入官方平台 |
| `commerce/workflow.py` | build_workflow、run_analysis | 谁先执行，数据怎样流动 |
| `commerce/data.py` | periods、reference_rows | 时间范围和独立答案怎样得到 |
| `commerce/query.py` | STANDARD_SQL、execute_readonly、validate_rows | 如何查，如何拒绝错误结果 |
| `commerce/analysis.py` | decompose | 四项贡献怎样算 |
| `commerce/report.py` | provenance、save_result、render_report | 什么证据落盘，谁生成数字 |
| `commerce/cli.py` | main 的 replay 分支 | 如何复算历史结果 |

推荐先读 `build_workflow` 中四个节点的依赖关系，再顺着四个函数阅读。先形成全局调用顺序，再读 SQL 和数学细节，理解成本会低很多。

## 几个容易看不懂的代码机制

### tool 装饰器与工具描述

普通 Python 函数只能由知道它的代码调用。`@tool` 将函数暴露为框架可登记的工具资源，函数名、描述与参数信息帮助模型决定是否调用。`register_resource` 让框架执行层能找到这个资源；提示词中的描述让模型决策层知道它能做什么。前者存在、后者缺失，就会出现“明明注册了，Agent 却不用”的问题。

外层的 ReAct 可以理解为反复进行“选择动作 → 调用工具 → 观察结果 → 决定下一步”。在本例成功路径中，观察到分析工具返回 verified 和报告路径后，下一步是展示文件并结束。当前业务 DAG 不会因为使用了外层 ReAct 就变成开放式多 Agent 系统。

### 为什么调用最后一个节点就能跑完整流程

`start >> sql >> check >> calc` 建立的是上游到下游的依赖。构图完成后，`leaf.call(call_data=state)` 以末端节点为目标交给 AWEL 执行；引擎先满足其上游依赖，再运行当前节点。因此它不是绕过前三步直接调用 calculation。

依赖关系同时限定了成功数据流：SQL 没有通过核验，正常情况下就不会进入贡献计算。这个顺序来自代码定义，模型不能通过一句“跳过校验”改掉 DAG 里的连线。

### async 和 to_thread 分别处理什么

模型请求会等待网络返回，使用 async 可以让事件循环在等待期间处理其他工作；SQLite 查询和 Python 明细循环属于同步函数，`asyncio.to_thread` 把它们交给线程，避免直接堵住异步服务的事件循环。

这不意味着四个 DAG 节点并行计算。它们有数据依赖，仍然按顺序执行。线程转移也不等于 CPU 密集型 Python 运算获得线性多核加速，更不代表官方入口已有全局并发配额。

### 为什么 state 要记录尝试列表

只保存最终 SQL 会丢失第一次失败的信息。`sql_attempts` 每次追加候选，并标注 source、rows、status、error，可以解释“第一条 SQL 为什么错、第二条如何修复”。独立核验结果放在 `independent_reference`，最终计算输入放在 `rows`，这两者让审阅者能重做比较。

当前节点会原地更新同一个字典，并非每步产生不可变版本。这个实现简单，适合串行链路；如果未来引入分支并行，就要明确哪些字段由谁写入、如何合并，不能直接让多个节点同时修改同一列表。

### 异常怎样走回用户

SQL 执行或结果检查抛出 `QueryRejected`，由 verification 决定是否修复；超出修复预算或其他步骤失败后，异常进入 `run_analysis` 的捕获分支，标记 failed，保存证据，并抛出包含运行编号的 `WorkflowFailure`。官方业务工具没有在内部把它伪装成 verified，因此成功报告不会沿正常返回路径产生。

`verified` 表示本次专用计算链路通过了定义好的检查。它不表示外层全部自然语言已经逐字核验，也不表示所有可能的业务问题都得到正确解释。
